package audio;

import calculators.FourierCalculator;
import calculators.Windowing;
import gui.GraphPanel;
import gui.ViewPane;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.TargetDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

/**
 * Record allows recording of audio from microphone input to a file.
 * @author Peter Schilling, Jeff Hunter
 */
public class WavWriter {

	public static final int SAMPLE_SIZE = 8;
	public static final int CHANNELS = 1;
	public static final int BUFFER_SIZE = 4096;
	public static final boolean SIGNED = false;
	public static final int NO_WINDOW = 0;
	public static final int HAMMING_WINDOW = 1;
	public static final int TRIANGULAR_WINDOW = 2;
	private float sampleRate_ = 44100;
	private int numBins_ = 64;
	private File currentFile_;
	private GraphPanel timeDomainGraph_;
	private GraphPanel freqDomainGraph_;
	private AudioInputStream audioStream_;
	private TargetDataLine microphoneLine_;
	private DataLine.Info dataLineInfo_;
	private CaptureThread captureThread_;
	private PlayThread playThread_;
	private AudioFormat format_;
	private Clip clip_;
	private double filter_[];
	private int windowMode_;

	public WavWriter(File currentFile, ViewPane vp) {
		format_ = new AudioFormat(sampleRate_, SAMPLE_SIZE, CHANNELS, SIGNED, true);
		currentFile_ = currentFile;
		dataLineInfo_ = new DataLine.Info(TargetDataLine.class, format_);
		timeDomainGraph_ = vp.getTopGraph();
		freqDomainGraph_ = vp.getBottomGraph();
		filter_ = new double[numBins_];
		for (int i = 0; i < filter_.length; i++)
			filter_[i] = 0;
		windowMode_ = NO_WINDOW;
	}

	public void graphAudio() {
		byte[] buffer = new byte[BUFFER_SIZE];
		freqDomainGraph_.deleteAll();
		timeDomainGraph_.deleteAll();
		updateFileStream();
		try {
			int length;
			while ((length = audioStream_.read(buffer)) > 0)
				for (int i = 0; i < length; i += 32) {
					int peakSample = 0;
					for (int j = i; j < i + 32; j++)
						if (Math.abs((int) buffer[j] & 0x000000FF) > Math.abs(peakSample))
							peakSample = ((int) buffer[j] & 0x000000FF) - 128;
					timeDomainGraph_.addPoint(peakSample);
				}
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		switch (windowMode_) {
			case NO_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(buffer, numBins_));
				break;
			case HAMMING_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(Windowing.hamming(buffer), numBins_));
				break;
			case TRIANGULAR_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(Windowing.triangular(buffer), numBins_));
				break;
		}
		timeDomainGraph_.scrollToEnd();
	}

	public void updateFreqDomain(int startPixel, int endPixel, double graphLength) {
		updateFileStream();
		double bytesPerPixel = (double) audioStream_.getFrameLength() / graphLength;
		int selectedLength = (int) ((double) (endPixel - startPixel) * bytesPerPixel);
		int startByte = (int) ((double) startPixel * bytesPerPixel);
		byte fourierBytes[] = new byte[selectedLength];

		try {
			audioStream_.skip(startByte); //read until delete start
			audioStream_.read(fourierBytes);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		freqDomainGraph_.deleteAll();
		switch (windowMode_) {
			case NO_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(fourierBytes, numBins_));
				break;
			case HAMMING_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(Windowing.hamming(fourierBytes), numBins_));
				break;
			case TRIANGULAR_WINDOW:
				freqDomainGraph_.addPoints(FourierCalculator.amplitude(Windowing.triangular(fourierBytes), numBins_));
				break;
		}
	}

	public void delete(int startPixel, int endPixel, double graphLength) {
		updateFileStream();
		byte buffer[] = new byte[BUFFER_SIZE];
		double bytesPerPixel = (double) audioStream_.getFrameLength() / graphLength;
		int toDeleteLength = (int) ((double) (endPixel - startPixel) * bytesPerPixel);
		int startByte = (int) ((double) startPixel * bytesPerPixel);
		byte start[] = new byte[startByte];
		ByteArrayOutputStream bOut = new ByteArrayOutputStream(BUFFER_SIZE);

		try {
			audioStream_.read(start); //read until delete start
			bOut.write(start);
			audioStream_.skip(toDeleteLength);
			int length;
			while ((length = audioStream_.read(buffer)) > 0)
				bOut.write(buffer, 0, length);
			audioStream_ = new AudioInputStream(new ByteArrayInputStream(bOut.toByteArray()), format_, bOut.size());
			AudioSystem.write(audioStream_, AudioFileFormat.Type.WAVE, currentFile_);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		graphAudio();
	}

	public byte[] cut(int startPixel, int endPixel, double graphLength) {
		updateFileStream();
		byte buffer[] = new byte[BUFFER_SIZE];
		double bytesPerPixel = (double) audioStream_.getFrameLength() / graphLength;
		int cutLength = (int) ((double) (endPixel - startPixel) * bytesPerPixel);
		int startByte = (int) ((double) startPixel * bytesPerPixel);
		byte start[] = new byte[startByte];
		byte cutBytes[] = new byte[cutLength];

		ByteArrayOutputStream bOut = new ByteArrayOutputStream(BUFFER_SIZE);

		try {
			audioStream_.read(start); //read until delete start
			bOut.write(start);
			audioStream_.read(cutBytes);
			int length;
			while ((length = audioStream_.read(buffer)) > 0)
				bOut.write(buffer, 0, length);
			audioStream_ = new AudioInputStream(new ByteArrayInputStream(bOut.toByteArray()), format_, bOut.size());
			AudioSystem.write(audioStream_, AudioFileFormat.Type.WAVE, currentFile_);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		graphAudio();

		return cutBytes;
	}

	public byte[] copy(int startPixel, int endPixel, double graphLength) {
		updateFileStream();
		double bytesPerPixel = (double) audioStream_.getFrameLength() / graphLength;
		int copyLength = (int) ((double) (endPixel - startPixel) * bytesPerPixel);
		int startByte = (int) ((double) startPixel * bytesPerPixel);
		byte copyBytes[] = new byte[copyLength];

		try {
			audioStream_.skip(startByte); //read until delete start
			audioStream_.read(copyBytes);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		return copyBytes;
	}

	public void paste(byte[] pasteData, int startPixel, double graphLength) {
		updateFileStream();
		byte buffer[] = new byte[BUFFER_SIZE];
		double bytesPerPixel = (double) audioStream_.getFrameLength() / graphLength;
		int startByte = (int) ((double) startPixel * bytesPerPixel);
		byte start[] = new byte[startByte];
		ByteArrayOutputStream bOut = new ByteArrayOutputStream();

		try {
			audioStream_.read(start); //read until delete start
			bOut.write(start);
			bOut.write(pasteData);
			int length;
			while ((length = audioStream_.read(buffer)) > 0)
				bOut.write(buffer, 0, length);
			audioStream_ = new AudioInputStream(new ByteArrayInputStream(bOut.toByteArray()), format_, bOut.size());
			AudioSystem.write(audioStream_, AudioFileFormat.Type.WAVE, currentFile_);
		} catch (IOException ex) {
			ex.printStackTrace();
		}
		graphAudio();
	}

	/**
	 * Start recording from microphone
	 */
	public void startRecord() {
		try {
			format_ = new AudioFormat(sampleRate_, SAMPLE_SIZE, CHANNELS, SIGNED, true);
			dataLineInfo_ = new DataLine.Info(TargetDataLine.class, format_);
			microphoneLine_ = (TargetDataLine) AudioSystem.getLine(dataLineInfo_);
			captureThread_ = new CaptureThread();
			captureThread_.start();
		} catch (LineUnavailableException ex) {
			System.err.println("Line unavailable, stop other programs using audio");
		}
	}

	/**
	 * Thread for audio recording so it doesn't lock up the program
	 */
	class CaptureThread extends Thread {

		private byte[] buffer = new byte[BUFFER_SIZE];
		ByteArrayOutputStream bOut = new ByteArrayOutputStream();

		@Override
		public void run() {
			try {
				timeDomainGraph_.deleteAll();
				microphoneLine_.open(format_, BUFFER_SIZE);
				microphoneLine_.start();

				int length;
				while ((length = microphoneLine_.read(buffer, 0, BUFFER_SIZE)) > 0) {
				
					for (int i = 0; i < length; i += 32) {
						int peakSample = 0;
						for (int j = i; j < i + 32; j++)
							if (Math.abs((int) buffer[j] & 0x000000FF) > Math.abs(peakSample))
								peakSample = ((int) buffer[j] & 0x000000FF) - 128;

						timeDomainGraph_.addPoint((double) peakSample);
						timeDomainGraph_.scrollToEnd();
					}
					bOut.write(buffer, 0, length);
				//freqDomainGraph_.deleteAll();
				//freqDomainGraph_.addPoints(FourierCalculator.amplitude(buffer, 256));
				// that would be cool...
				}
				audioStream_ = new AudioInputStream(new ByteArrayInputStream(bOut.toByteArray()), format_, bOut.size());
				int bytesRecorded = AudioSystem.write(audioStream_, AudioFileFormat.Type.WAVE, currentFile_);
				System.out.println(bytesRecorded + " bytes recorded");
			} catch (IOException ex) {
				ex.printStackTrace();
			} catch (LineUnavailableException ex) {
				System.err.println("Line unavailable, stop other programs using audio");
			}
		}
	}

	/**
	 * Stop current recording
	 */
	public void stopRecord() {
		microphoneLine_.close();
		try {
			Thread.sleep(5); // allows for file to finish writting
		} catch (InterruptedException ex) {
		}
		graphAudio();
	}

	public void playFile() {
		try {
			updateFileStream();
			clip_ = AudioSystem.getClip();
			playThread_ = new PlayThread();
			playThread_.start();
		} catch (LineUnavailableException ex) {
			System.err.println("Line not available, close other applications using audio and try again");
		}
	}

	/**
	 * Thread for audio playing so it doesn't lock up the program
	 */
	class PlayThread extends Thread {

		@Override
		public void run() {
			try {
				clip_.open(audioStream_);
				if (clip_ != null)
					clip_.loop(1);
				Thread.sleep(clip_.getMicrosecondLength() / 1000);
			} catch (LineUnavailableException ex) {
				System.err.println("Line unavailable, stop other programs using audio");
			} catch (IOException ex) {
				ex.printStackTrace();
			} catch (InterruptedException ex) {
				ex.printStackTrace();
			}
			clip_.close();
		}
	}

	public void stopPlayFile() {
		playThread_.interrupt();
	}

	private void updateFileStream() {
		updateFileStream(0);
	}

	private void updateFileStream(int tries) {
		int tries_ = tries;
		try {
			//AudioSystem.getAudioFileFormat(currentFile_);
			audioStream_ = AudioSystem.getAudioInputStream(currentFile_);
			format_ = audioStream_.getFormat();
		} catch (UnsupportedAudioFileException ex) {
			System.err.println("File not supported");
		} catch (IOException ex) {
			System.err.println("io exception " + ++tries_);
			try {
				Thread.sleep(5);
				if (tries_ <= 3)
					updateFileStream(tries_);
				else
					ex.printStackTrace();
			} catch (InterruptedException ex1) {
				ex1.printStackTrace();
			}
		}
	}

	public void newFilter(int startPixel, int endPixel, int panelWidth) {
		if (startPixel >= panelWidth / 2) {
			//entire selection is right of n. limit
			final int tempEnd = endPixel;
			endPixel = panelWidth - startPixel;
			startPixel = panelWidth - tempEnd;
		}
		if (endPixel >= panelWidth / 2)
			//start is on left of n.q. limit
			if (endPixel - panelWidth / 2 <= Math.abs(panelWidth / 2 - startPixel))
				endPixel = panelWidth / 2;
			else {
				startPixel = panelWidth - endPixel;
				endPixel = panelWidth / 2;
			}

		final double pixelsPerBin = ((double) panelWidth / (double) numBins_);
		final int startBin = (int) ((double) startPixel / pixelsPerBin);
		final int endBin = (int) ((double) endPixel / pixelsPerBin);
		
		filter_ = new double[numBins_];

		for (int i = 0; i < startBin; i++)
			filter_[i] = 0;
		for (int i = startBin; i <= endBin; i++)
			filter_[i] = 1;
		for (int i = endBin + 1; i < filter_.length / 2; i++)
			filter_[i] = 0;
		for (int i = filter_.length - 1, j = 0; i > filter_.length / 2 - 1; i--, j++)
			filter_[i] = filter_[j];

		filter_ = FourierCalculator.inverse(filter_);
		updateFileStream();

		ArrayList<Double> filteredValues = new ArrayList<Double>(filter_.length);
		ByteArrayOutputStream bOut = new ByteArrayOutputStream();
		long audioStreamLength = audioStream_.getFrameLength() * format_.getFrameSize();
		double cSample;
		try {
			for (long i = 0; i < audioStreamLength + filter_.length - 1; i++) {
				cSample = (double) audioStream_.read();
				if (cSample == -1.0)
					cSample = 128;
				cSample -= 128;
				filteredValues.add(0.0);
				for (int j = 1; j <= filteredValues.size(); j++)
					filteredValues.set(j - 1, ((filter_[filteredValues.size() - j]) * cSample) + filteredValues.get(j - 1));
				if (filteredValues.size() == filter_.length)
					bOut.write((int) (filteredValues.remove(0) + 128));
			}

			audioStream_ = new AudioInputStream(new ByteArrayInputStream(bOut.toByteArray()), format_, bOut.size());
			AudioSystem.write(audioStream_, AudioFileFormat.Type.WAVE, currentFile_);

			audioStream_.close();
			graphAudio();

		} catch (IOException ex) {
			ex.printStackTrace();
		}
	}

	public void setNumBins(int numBins) {
		numBins_ = numBins;
		graphAudio();
	}

	public void setWindowMode(int windowMode) {
		windowMode_ = windowMode;
		graphAudio();

	}

	public int getNumBins() {
		return numBins_;
	}
	
	public int getVisualSampleRate() {
		if(audioStream_ != null)
			return (int) audioStream_.getFormat().getSampleRate() / 32; //displayed points
		else
			return (int)sampleRate_ / 32;
	}

	public int getSampleRate() {
		if(audioStream_ != null)
			return (int) audioStream_.getFormat().getSampleRate(); 
		else
			return (int)sampleRate_;
	}

	public void setSampleRate(float sampleRate) {
		this.sampleRate_ = sampleRate;
		System.out.println("sr=" + sampleRate);
	}
	
	
	
	public String getWindowMode() {
		switch (windowMode_) {
			case NO_WINDOW:
				return "none";
			case HAMMING_WINDOW:
				return "hamming";
			case TRIANGULAR_WINDOW:
				return "triangular";
		}
		return "error";
	}
}
